[15-最兼容标准库的日志库-logrus] 一 logrus介绍 1.1 log标准库优缺点 优点
Go标准库的log日志库非常简单
可以便设置任何io.Writer
作为日志输出位置
缺点
1 仅仅提供了print,panic和fatal三个函数,不支持info/debug等多个级别
2 记录错误有Fatal和Panic;Fatal通过调用os.Exit(1)
来结束程序;Panic在写入日志后抛出一个panic;缺少ERROR日志级别,在不抛出异常和退出程序的情况下记录日志
3 不支持多输出 - 同时支持标准输出,文件等
4 缺乏日志格式化的能力,例如:记录函数名和行号,格式化日期和时间格式等
5 可读性与结构化差,没有Json格式或有分隔符,不方便后续的日志采集、监控等
6 对于更精细的日志级别、日志文件分割,以及日志分发等,没有提供支持
1.2 Go中常用第三方日志库 在Go的世界,流行的日志框架有logrus、zap、zerolog等
logrus 目前Github上star数量最多的日志库
项目地址: https://github.com/sirupsen/logrus
Stars数量:20.3k
zap 是Uber推出的一个快速、结构化的分级日志库
项目地址:https://github.com/uber-go/zap
官方文档:https://pkg.go.dev/go.uber.org/zap
Stars数量:20.3k
zerolog 它的 API 设计非常注重开发体验和性能。zerolog
只专注于记录 JSON 格式的日志,号称 0 内存分配
项目地址:https://github.com/rs/zerolog
Stars数量:6.2k
二 logrus 2.1 logrus特点 优点
完全兼容Go标准库日志模块:logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是golang标准库日志模块的API的超集。如果之前项目使用标准库日志模块,完全可以以最低的代价迁移到logrus上
可扩展的Hook机制:允许使用者通过hook的方式将日志分发到任意地方,如本地文件系统、标准输出、linfluxdb、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等
可选的日志输出格式:logrus内置了两种日志格式,JSONFormatter和TextFormatter,如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式
Field机制:logrus鼓励通过Field机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志
logrus是一个可插拔的、结构化的日志框架,很多开源项目,如docker,prometheus等,都是用了logrus来记录其日志
缺点 尽管 logrus有诸多优点,但是为了灵活性和可扩展性,官方也削减了很多实用的功能,例如:
没有提供行号和文件名的支持
输出到本地文件系统没有提供日志分割功能
官方没有提供输出到ELK等日志处理中心的功能
但是这些功能都可以通过自定义hook来实现
2.2 logrus配置 日志级别 logrus有7个日志级别,依次是Trace –> Debug –> Info –> Warning –>Error –> Fatal –>Panic
1 2 logrus.SetLevel(logrus.DebugLevel)
日志格式 logrus内置了JSONFormatter
和TextFormatter
两种格式,也可以通过Formatter
接口定义日志格式
1 2 3 4 5 6 7 8 9 10 11 12 13 logrus.SetFormatter(&logrus.TextFormatter{ ForceColors: true , EnvironmentOverrideColors: true , TimestampFormat: "2006-01-02 15:04:05" , }) logrus.SetFormatter(&logrus.JSONFormatter{ PrettyPrint: false , TimestampFormat: "2006-01-02 15:04:05" , })
输出文件 1 2 3 logfile, _ := os.OpenFile("./log.log" , os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644 ) logrus.SetOutput(logfile)
日志定位 定位行号(如:func=main.main file="./xxx.go:38"
)
1 logrus.SetReportCaller(true )
2.3 快速使用
go get github.com/sirupsen/logrus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "github.com/sirupsen/logrus" "os" ) func init () { logrus.SetLevel(logrus.DebugLevel) logrus.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: "2006-01-02 15:04:05" , }) logfile, _ := os.OpenFile("./app.log" , os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644 ) logrus.SetOutput(logfile) logrus.SetReportCaller(true ) } func main () { logrus.Infoln("info--日志数据" ) logrus.Debugln("debug--日志数据" ) logrus.Errorln("err--日志数据" ) }
TextFormatter 1 2 3 4 5 6 7 8 9 10 11 type TextFormatter struct { DisableColors bool DisableTimestamp bool TimestampFormat string QuoteEmptyFields bool CallerPrettyfier func (*runtime.Frame) (function string , file string ) }
1 2 3 4 5 6 7 8 9 type JSONFormatter struct { TimestampFormat string DisableTimestamp bool CallerPrettyfier func (*runtime.Frame) (function string , file string ) PrettyPrint bool }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 type Formatter interface { Format(*Entry) ([]byte , error ) } type Entry struct { Data Fields Time time.Time Level Level Caller *runtime.Frame Message string Buffer *bytes.Buffer }
2.5 日志打印方法 FieldLogger接口: FieldLogger
定义了所有日志打印的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type FieldLogger interface { WithField(key string , value interface {}) *Entry WithFields(fields Fields) *Entry WithError(err error ) *Entry Debugf(format string , args ...interface {}) Infof(format string , args ...interface {}) Printf(format string , args ...interface {}) Warnf(format string , args ...interface {}) Warningf(format string , args ...interface {}) Errorf(format string , args ...interface {}) Fatalf(format string , args ...interface {}) Panicf(format string , args ...interface {}) Debug(args ...interface {}) Info(args ...interface {}) Print(args ...interface {}) Warn(args ...interface {}) Warning(args ...interface {}) Error(args ...interface {}) Fatal(args ...interface {}) Panic(args ...interface {}) Debugln(args ...interface {}) Infoln(args ...interface {}) Println(args ...interface {}) Warnln(args ...interface {}) Warningln(args ...interface {}) Errorln(args ...interface {}) Fatalln(args ...interface {}) Panicln(args ...interface {}) }
2.6 logrus实例 实例日志打印方式一 默认实例 (函数)
,即通过logrus包提供的函数(覆盖了FieldLogger
接口的所有方法),直接打印日志。但其实logrus包函数是调用了logrus.Loger
默认实例。
1 2 3 4 5 func main () { logrus.Infoln("info--日志" ) logrus.Errorln("err--日志" ) }
实例日志打印方式二 Logger实例(对象)
,它实现了FieldLogger
接口。
1 2 3 4 5 6 func main () { var loger = logrus.StandardLogger() loger.Formatter = &logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05" } loger.Infoln("info--日志" ) }
实例日志打印方式三 Entry示例(对象)
,它也实现了FieldLogger
接口,是最终是日志打印入口。
这里用到了Field
机制,logrus鼓励通过Field
机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志。
1 2 3 4 5 6 7 8 9 func main () { logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05" }) entry := logrus.WithFields(logrus.Fields{ "global" : "全局字段-每个日志都会输出" , }) entry.WithFields(logrus.Fields{"module" : "自定义字段--用户模块" }).Info("info--日志" ) entry.WithFields(logrus.Fields{"module" : "自定义字段--商品模块" }).Error("Error--日志" ) }
2.7 HOOK机制
hook即钩子,拦截器。它为logrus提供了强大的功能扩展,如将日志分发到任意地方,如本地文件系统、logstash
、es
等,或者切割日志、定义日志内容和格式等。hook接口原型如下:
1 2 3 4 type Hook interface { Levels() []Level Fire(*Entry) error }
Hook - 实现日志切割功能
需要借助于第三方(日志轮转库):github.com/lestrrat-go/file-rotatelogs 和:github.com/rifflock/lfshook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package mainimport ( "github.com/lestrrat-go/file-rotatelogs" "github.com/rifflock/lfshook" "github.com/sirupsen/logrus" "time" ) func main () { hook := NewLfsHook("app_hook" , time.Second*2 , 5 ) logrus.AddHook(hook) logrus.Infoln("info---测试开始" ) log := logrus.WithFields(logrus.Fields{"module" : "用户模块" }) for i := 0 ; i < 15 ; i++ { log.Infoln("info--->成功" , i) time.Sleep(time.Second) log.Errorln("err--->成功" , i) } } func NewLfsHook (logName string , rotationTime time.Duration, leastDay uint ) logrus.Hook { writer, err := rotatelogs.New( logName+".%Y%m%d%H%M%S" , rotatelogs.WithRotationTime(rotationTime), rotatelogs.WithRotationCount(leastDay), ) if err != nil { panic (err) } lfsHook := lfshook.NewHook(lfshook.WriterMap{ logrus.DebugLevel: writer, logrus.InfoLevel: writer, logrus.WarnLevel: writer, logrus.ErrorLevel: writer, logrus.FatalLevel: writer, logrus.PanicLevel: writer, }, &logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05" }) return lfsHook }
Hook - 写入Redis 将日志输出到redis
需要借助于第三方模块:github.com/rogierlommers/logrus-redis-hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( logredis "github.com/rogierlommers/logrus-redis-hook" "github.com/sirupsen/logrus" ) func init () { hookConfig := logredis.HookConfig{ Host: "localhost" , Key: "test" , Format: "v1" , App: "my_app_name" , Port: 6379 , Hostname: "my_app_hostname" , DB: 0 , TTL: 3600 , } hook, err := logredis.NewHook(hookConfig) if err == nil { logrus.AddHook(hook) } else { logrus.Errorf("日志写入redis配置出错: %q" , err) } } func main () { logrus.WithFields(logrus.Fields{"module" : "用户模块" }).Info("info--日志--写入redis" ) logrus.WithFields(logrus.Fields{"module" : "用户模块" }).Error("Error--日志--写入redis" ) }
其他Hook
2.8 Fatal处理 logrus的Fatal
输出,会执行os.Exit(1)
。logrus提供RegisterExitHandler
方法,可以在系统异常时调用一些资源释放api等,让应用正确地关闭。
1 2 3 4 5 6 7 8 9 10 func main () { logrus.RegisterExitHandler(func () { fmt.Println("发生了fatal异常,执行关闭文件等工作" ) }) logrus.Warnln("warn测试" ) logrus.Fatalln("fatal测试" ) logrus.Infoln("info测试" ) }
三 Gin中集成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "io" "os" ) func init () { logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05" }) logfile, _ := os.OpenFile("./app.log" , os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644 ) logrus.SetOutput(io.MultiWriter(os.Stdout, logfile)) gin.DisableConsoleColor() gin.DefaultWriter = io.MultiWriter(os.Stdout, logfile) } func main () { log := logrus.WithFields(logrus.Fields{ "module" : "用户模块" , }) r := gin.Default() r.GET("/" , func (c *gin.Context) { log.Infoln("info--->gin日志数据" ) c.String(200 , "ok" ) }) r.Run(":8080" ) }
四 logrus线程安全
默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。
互斥锁在调用hooks或者写日志的时候执行。
如果不需要锁,可以调用logger.SetNoLock()
来关闭。
可以关闭logrus互斥锁的情形:
没有设置hook,或者所有的hook都是线程安全的实现。
写日志到logger.Out已经是线程安全的了。例如,logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND方式打开的,并且每次写操作都小于4k。